package ca.tatham.triplipse.editors;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.editors.text.TextEditor;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.ui.texteditor.MarkerUtilities;
import org.openrdf.model.Resource;
import org.openrdf.model.URI;
import org.openrdf.model.Value;
import org.openrdf.rio.NamespaceListener;
import org.openrdf.rio.ParseErrorListener;
import org.openrdf.rio.ParseException;
import org.openrdf.rio.ParseLocationListener;
import org.openrdf.rio.Parser;
import org.openrdf.rio.StatementHandler;
import org.openrdf.rio.StatementHandlerException;

import ca.tatham.triplipse.Activator;
import ca.tatham.triplipse.RdfStatement;
import ca.tatham.triplipse.RioRdfStatement;

public abstract class RdfEditor extends TextEditor implements
		IResourceChangeListener {

	private LineNumberListener m_lineNumberListener;

	public RdfEditor() {
		super();
		ResourcesPlugin.getWorkspace().addResourceChangeListener(this,
				IResourceChangeEvent.POST_CHANGE);
	}

	protected abstract String getMarkerType();

	protected abstract Parser getParser();

	public final void dispose() {
		ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
		super.dispose();
	}

	public final void init(IEditorSite site, IEditorInput editorInput)
			throws PartInitException {
		if (!(editorInput instanceof IFileEditorInput))
			throw new PartInitException(
					"Invalid Input: Must be IFileEditorInput");
		super.init(site, editorInput);
		processText();
	}

	public void setLineNumber(int lineNumber) {
		IRegion line = getLineInformation(lineNumber);
		if (line != null) {
			getSourceViewer().setSelectedRange(line.getOffset(),
					line.getLength());
		}
	}

	IRegion getLineInformation(int lineNumber) {
		IRegion line = null;
		try {
			if (getSourceViewer() != null) {
				IDocument textDocument = getSourceViewer().getDocument();
				if (textDocument != null) {
					line = textDocument.getLineInformation(lineNumber);
				}
			}
		} catch (BadLocationException e) {
			e.printStackTrace();
		}
		return line;
	}

	/**
	 * Parses RDF whenever the file is saved
	 * 
	 * @see IResourceChangeListener#resourceChanged(IResourceChangeEvent)
	 */
	public final void resourceChanged(final IResourceChangeEvent event) {
		if (isThisFile(event) && !isEventCausedByNewMarker(event)) {
			Display.getDefault().asyncExec(new Runnable() {
				public void run() {
					processText();
				}
			});
		}
	}

	/**
	 * @param event
	 *            the event to check
	 * @return true if the given event was triggered because of a new marker
	 *         being added
	 */
	private boolean isEventCausedByNewMarker(final IResourceChangeEvent event) {
		return event.findMarkerDeltas(getMarkerType(), false).length > 0;
	}

	protected final void processText() {
		final List<RdfStatement> statements = new ArrayList<RdfStatement>();
		final Map<String, String> prefixes = new HashMap<String, String>();
		try {
			parse(this, getParser(), new RdfStatementCollector(statements),
					new PrefixListener(prefixes));
		} catch (IOException e) {
			e.printStackTrace();
		} catch (StatementHandlerException e) {
			e.printStackTrace();
		}
		Activator.getDefault().getRdfDocument()
				.setTriples(statements, prefixes);
	}

	protected final boolean isThisFile(final IResourceChangeEvent event) {
		IFile thisFile = getRdfFile();
		return isResourceAffected(thisFile, event.getDelta()
				.getAffectedChildren());
	}

	protected final IFile getRdfFile() {
		return ((FileEditorInput) getEditorInput()).getFile();
	}

	private final boolean isResourceAffected(IResource resource,
			IResourceDelta[] deltas) {
		for (int i = 0; i < deltas.length; i++) {
			if (deltas[i].getResource().equals(resource)) {
				return true;
			}
			return isResourceAffected(resource, deltas[i].getAffectedChildren());
		}
		return false;
	}

	private final void parse(final ITextEditor sourceEditor,
			final Parser rdfParser, final StatementHandler handler,
			final NamespaceListener namespaceListener) throws IOException,
			StatementHandlerException {
		String inputText = sourceEditor.getDocumentProvider().getDocument(
				sourceEditor.getEditorInput()).get();

		Reader reader = new BufferedReader(new StringReader(inputText));
		rdfParser.setParseLocationListener(getLineNumberListener());
		rdfParser.setParseErrorListener(new RdfParseErrorListener());
		rdfParser.setStatementHandler(handler);
		rdfParser.setNamespaceListener(namespaceListener);
		rdfParser.setStopAtFirstError(false);
		deleteMarkers();
		try {
			rdfParser.parse(reader, "");
		} catch (ParseException e) {
			// do nothing - caught by error listener.
		}
	}

	LineNumberListener getLineNumberListener() {
		if (m_lineNumberListener == null) {
			m_lineNumberListener = new LineNumberListener();
		}
		return m_lineNumberListener;
	}

	private void deleteMarkers() {
		try {
			getRdfFile().deleteMarkers(getMarkerType(), true, 0);
		} catch (CoreException e) {
			Activator.getDefault().getLog().log(
					new Status(IStatus.WARNING, Activator.PLUGIN_ID, 21,
							"Could not delete markers", e));
		}
	}

	class LineNumberListener implements ParseLocationListener {

		private int m_lineNumber = -1;

		public void parseLocationUpdate(int lineNo, @SuppressWarnings("unused")
		int columnNo) {
			m_lineNumber = lineNo;
		}

		int getLineNumber() {
			return m_lineNumber;
		}
	}

	class RdfStatementCollector implements StatementHandler {

		private final List<RdfStatement> m_statements;

		public RdfStatementCollector(List<RdfStatement> statements) {
			m_statements = statements;
		}

		public void handleStatement(Resource subject, URI predicate,
				Value object) {
			m_statements.add(new RioRdfStatement(subject, predicate, object,
					getLineNumberListener().getLineNumber() - 1));
		}

	}

	class PrefixListener implements NamespaceListener {
		private final Map<String, String> m_prefixes;

		public PrefixListener(Map<String, String> prefixes) {
			m_prefixes = prefixes;
		}

		public void handleNamespace(String prefix, String uri) {
			m_prefixes.put(prefix, uri);
		}
	}

	class RdfParseErrorListener implements ParseErrorListener {

		public void error(String msg, int lineNo, int colNo) {
			createMarker(msg, lineNo, colNo, IMarker.SEVERITY_ERROR);
		}

		public void fatalError(String msg, int lineNo, int colNo) {
			createMarker(msg, lineNo, colNo, IMarker.SEVERITY_ERROR);
		}

		public void warning(String msg, int lineNo, int colNo) {
			createMarker(msg, lineNo, colNo, IMarker.SEVERITY_WARNING);
		}

		private void createMarker(String msg, int lineNo, int colNo,
				int severity) {
			Map<String, Object> attributes = new HashMap<String, Object>();
			attributes.put(IMarker.LINE_NUMBER, lineNo - 1);
			IRegion lineInfo = getLineInformation(lineNo - 1);
			if (lineInfo != null) {
				int offset = lineInfo.getOffset();
				int charStart = colNo < 1 ? offset : offset + colNo - 1;
				attributes.put(IMarker.CHAR_START, charStart);
				attributes.put(IMarker.CHAR_END, offset + lineInfo.getLength()
						- 1);
			}
			attributes.put(IMarker.TRANSIENT, true);
			attributes.put(IMarker.LOCATION, "Column: " + colNo);
			attributes.put(IMarker.MESSAGE, msg.replace("\n", "\\n"));
			attributes.put(IMarker.USER_EDITABLE, false);
			attributes.put(IMarker.SEVERITY, severity);
			try {
				MarkerUtilities.createMarker(getRdfFile(), attributes,
						getMarkerType());
			} catch (CoreException e) {
				e.printStackTrace();
			}
		}

	}

}
